为什么生成模型中需要输入BOS和EOS等特殊标志

在使用预训练模型时,我们有时需要使用一些自定义 token 来增强输入,例如使用[ENT_START]和[ENT_END]在文本中标记出实体。由于自定义token并不在预训练模型原来的词表中,因此直接使用tokenizer处理数据会将自定义的特殊标记当作未知字符处理。或者在遇到一些领域中的专业术语时,往往这些术语不存在于词表当中,在tokenize时也会出现问题。这时就需要将这些标记、名词添加到tokenizer中。

添加新token

Huggingface的Transformers库中提供了两种方式来添加新token,分别是:

  • add_tokens()
    在词表的最后添加普通token,返回值为成功添加的token个数;
    函数中包括special_tokens参数,将其设置为true即代表添加的token为special_token
    1
    2
    tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
    print(tokenizer.add_tokens(["new_tok1", "my_new-tok2"])) # 2
  • add_special_tokens()
    添加包含特殊token的字典,键值从bos_token、eos_token、unk_token、sep_token、pad_token、cls_tpken、mask_token、additional_special_tokens中选择。如果被添加的token不在词表中,则被添加到词表的最后。添加后,可以通过属性来访问这些token。
    1
    2
    3
    4
    5
    tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
    special_tokens_dict = {"cls_token": "[MY_CLS]"}
    num_added_toks = tokenizer.add_special_tokens(special_tokens_dict)
    print("We have added", num_added_toks, "tokens") # We have added 1 tokens
    assert tokenizer.cls_token == "[MY_CLS]"
    特殊 token 的标准化 (normalization) 过程与普通 token 有一些不同,比如不会被小写。这里我们使用的是不区分大小写的 BERT 模型,因此分词后添加的普通 token [NEW_tok1] 和 [NEW_tok2] 都被处理为了小写,而特殊 token [NEW_tok3] 和 [NEW_tok4] 则维持大写,与 [CLS] 等自带特殊 token 保持一致。

调整Embedding矩阵

无论使用那种方式向词表中添加新token后,都需要重置token embedding矩阵的大小,也就是向矩阵中添加新token对应的embedding,这样模型才可以正常工作。

1
2
3
4
5
6
7
8
9
10
from transformers import AutoTokenizer, AutoModel
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
model = AutoModel.from_pretrained("bert-base-uncased")
original_vocab_size = len(tokenizer)
num_added_toks = tokenizer.add_tokens(['[ENT_START]', '[ENT_END]'], special_tokens=True)
new_tokens_start_index = len(tokenizer) - original_vocab_size
model.resize_token_embeddings(len(tokenizer))
print(model.embeddings.word_embeddings.weight.size())
# Randomly generated matrix
print(model.embeddings.word_embeddings.weight[-2:, :])

token embedding初始化为已有token的值

如果有充分的训练语料对模型进行微调或者继续预训练,那么将新添加 token 初始化为随机向量没什么问题。但是如果训练语料较少,甚至是只有很少语料的 few-shot learning 场景下,这种做法就可能存在问题。研究表明,在训练数据不够多的情况下,这些新添加 token 的 embedding 只会在初始值附近小幅波动。换句话说,即使经过训练,它们的值事实上还是随机的。
比较常见的操作是根据新添加token的语义,将其值初始化为词表中已有token的embedding。例如对于上面的例子,我们可以将 [ENT_START] 和 [ENT_END] 的值都初始化为“entity”对应的 embedding。因为 token id 就是 token 在矩阵中的索引,因此我们可以直接通过 weight[token_id] 取出“entity”对应的 embedding。

1
2
3
4
5
token_id = tokenizer.convert_tokens_to_ids('entity')
token_embedding = model.embeddings.word_embeddings.weight[token_id]
with torch.no_grad():
for i in range(new_tokens_start_index,0,-1):
model.transformer.wte.weight[-i,:] = token_embedding.clone().detach().requires_grad_(True)

全0初始化

在很多情况下,我们需要手工初始化这些新 token 的 embedding。对于 Transformers 库来说,可以通过直接对 embedding 矩阵赋值来实现。例如对于上面的例子,我们将这两个新 token 的 embedding 都初始化为全零向量:

1
2
3
4
#初始化 embedding 的过程并不可导,因此这里通过 torch.no_grad() 暂停梯度的计算。
with torch.no_grad():
for i in range(new_tokens_start_index,0,-1):
model.transformer.wte.weight[-i,:] = torch.zeros([1,model.config.hidden_size],requires_grad=True)

参考链接:
1.为什么生成模型中需要输入BOS和EOS等特殊标志